Zbiór danych, który jest punktem wejściowym niniejszej analizy ma postać wyników badań próbek krwi, tj. jeden rekord odpowiada jednemu badaniu. Takie badanie zawierało zazwyczaj nie więcej niż 24 wartości różnych atrybutów, jednak biorąc pod uwagę fakt, że wszystkich atrybutów jest 74, zbiór w większości składa się z wartości pustych.
W wyniku wykonania analizy zbiorów częstych, udało się wyodrębnić 3 zbiory współwystępujących ze sobą atrybutów badań (support > 0.05) a do tego wyznaczyć dodatkowe pojedyncze atrybuty które były badane samodzielnie. Trzem wyodrębnionym zbiorom przypisano interpretacje medyczne, odpowiednio były to atrybuty zwiazane z: morfologią krwi, biochemią krwi i krzepliwością (koagulacją).
Aby zmniejszyć objętość zbioru, postanowiono scalić wyniki badań danych pacjentów, wykonanych w ciągu jednego dnia, w wypadku dwóch wartości dla jednego atrybutu wybierano ostatnią, jako najbardziej aktualną. Jednocześnie określono całkowitą ilość rodzajów badań dla każdego pacjenta, ze zbioru danych odrzucono pacjentów którzy mieli zbyt mało wykonanych badań.
Analiza płci i wieku pacjentów wskazała, że w badanej próbie, śmiertelność wśród kobiet jest mniejsza niż wśród mężczyzn, jednocześnie śmiertelność jest większa wśród osób starszych, niezależnie od płci.
Analiza korelacji wartości atrybutów wskazała wiele mocno ze sobą związanych atrybutów, jednak te zależności są łatwe do wyjaśnienia z perspektywy medycznej. Wyznaczono kilka czynników, które są istotnie skorelowane z atrybutem opisującym, czy pacjent przeżył chorobę COVID-19. Są to m.in.: stan układu odpornościowego (wysoka zawartość limfocytów, niska zawartość neutrofilów wśród białych krwinek), stan układu krzepnięcia, czynniki LDG i hs-CRP oraz większy poziom albumin.
Mimo tego, że dane były zbierane od 10 stycznia 2020, zasadnicza większość pacjentów została przyjęta do szpitala po 27 stycznia. Dla grupy pacjentów, którzy zmarli z powodu COVID-19 mediana czasu pobytu w szpitalu wyniosła 6 dni, a 75% z nich przebywała w szpitalu nie więcej niż 10 dni. Dla grupy pacjentów, którzy przeżyli chorobę, mediana czasu pobytu w szpitalu wyniosła 14 dni.
Stworzono dwa klasyfikatory - Random Forest i SVM, których celem było określenie, czy pacjent przeżyje chorobę COVID-19. Oba klasyfikatory wykazały się dokładnością powyżej 95% na zbiorze testowym. Dla celu stworzenia klasyfikatorów, wyniki badań zostały zagregowane do pacjentów, pozostawiając jedynie ostatnie (najbardziej aktualne) wartości dla każdego z atrybutów. Dodatkowo, klasyfikatory były trenowane jedynie na wybranym podzbiorze atrybutów, ze względu na znaczną część wartości pustych. Dla modelu Random Forest wyznaczono najważniejsze cechy, wśród których znalazły się parametry LDH, hs-CRP i zawartość limfocytów we krwi.
Ostatnią częścią raportu jest analiza przeżycia, wykonana przy użyciu modelu proporcjonalnego hazardu Coxa. W wyniku analizy uzyskano listy pozytywnych i negatywnych prognostyków, jednak uzyskane w trakcie tej analizy wyniki, mogą wymagać dalszej weryfikacji. Jednocześnie wśród pozytywnych prognostyków, czyli czynników zmniejszających ryzko śmierci w wyniku choroby, wskazano m.in wysoki poziom albumin, oraz wysoką zawartość limfocytów co jest w zgodzie z poprzednimi wnioskami.
library(readxl)
library(httr)
library(arules)
library(tidyr)
library(dplyr)
library(lubridate)
library(stringr)
library(ggplot2)
library(purrr)
library(broom)
library(tibble)
library(plotly)
library(gridExtra)
library(caret)
library(survival)
Odczyt danych przeprowadzony jest bezpośrednio ze strony, aby zapewnić stałą wartość początkową dla każdego uruchomienia. Dodatkowo uzupełniane są brakujące dane identyfikatory pacjentów PATIENT_ID. Wartości brakujące wynikają ze sposobu przetwarzania scalonych komórek pliku źródłowego.
# https://stackoverflow.com/a/41368947
url <- "http://www.cs.put.poznan.pl/dbrzezinski/teaching/zed/wuhan_blood_sample_data_Jan_Feb_2020.xlsx"
GET(url, write_disk(tf <- tempfile(fileext = ".xlsx")))
df <- read_excel(tf) %>% fill(PATIENT_ID)
Surowy zbiór danych zawiera 81 atrybutów i 6120 wierszy.
Atrybuty zbioru danych można potwierdzić na dwie grupy:
Poza danymi niedostępnymi (NA) w zbiorze danych występują dane brakujące, oznakowane wartością -1. Poniższa tabela pokazuje ilość takich wartości w każdej kolumnie, o ile te wartości występują.
| name | n |
|---|---|
| 2019-nCoV nucleic acid detection | 501 |
| Platelet count | 4 |
Dla kolumny Platelet count wartości brakujących jest mało, liczby -1 zostaną zamienione na NA.
df <- df %>% mutate(`Platelet count` = if(is.na(`Platelet count`) || `Platelet count` != -1) `Platelet count` else NA)
Dla kolumny 2019-nCoV nucleic acid detection sprawdzimy całkowitą ilość wartości różnych od NA.
| name | n |
|---|---|
| 2019-nCoV nucleic acid detection | 501 |
Wszystkie wartości w kolumnie 2019-nCoV nucleic acid detection różne od NA wynoszą -1, w związku z tym ta kolumna jest właściwie nieprzydatna. Mimo to na ten moment postanowiono nie zmieniać wartości w tej kolumnie, ewentualne zmiany można wykonać w dalszych krokach.
Pierwszych siedem kolumn to atrybuty ogólne, reprezentują następująco:
Nazwy wyżej wymienionych atrybutów zostaną przeformatowane, będą pisane wielkimi literami, a ewentualne spacje będą zamieniane na znak podkreślinka _, w ten sposób atrybuty ogólne będą łatwo odróżnialne od pozostałych atrybutów.
df <- df %>% rename_with(~ toupper(gsub(" ", "_", .x, fixed = TRUE)), 1:7)
Pola GENDER i OUTCOME zostaną dodatkowo zmienione na typ factor - z typu numerycznego zamienione zostaną na zmienne nominalne. Do tego trzeba przeprowadzić identyfikację typu płci.
Wartości płci nie są jawnie opisane, w artykule źródłowym została natomiast podana proporcja pacjentów danej płci, co powinno umożliwić określenie właściwego przypisania.
| GENDER | count |
|---|---|
| 1 | 224 |
| 2 | 151 |
Wniosek:
12df <- df %>%
mutate(across("GENDER", ~factor(., levels=c(1,2), labels=c("MALE", "FEMALE")))) %>%
mutate(across("OUTCOME", ~factor(., levels=c(0,1), labels=c("CURED","DECEASED"))))
Poniżej można zapoznać się z analizą wartości dla atrybutów ogólnych. Wartość atrybutu RE_DATE ma unikalną wartość dla każdego wiersza.
| RE_DATE | |
|---|---|
| Min. :2020-01-10 19:45:00 | |
| 1st Qu.:2020-02-04 13:44:00 | |
| Median :2020-02-09 12:42:30 | |
| Mean :2020-02-08 07:00:02 | |
| 3rd Qu.:2020-02-13 10:34:00 | |
| Max. :2020-02-18 17:49:00 | |
| NA’s :14 |
Pozostałe z atrybutów ogólnych, są wspólne dla każdego pacjenta, zastosowano transformację do zniwelowania skutków duplikacji.
| PATIENT_ID | AGE | GENDER | ADMISSION_TIME | DISCHARGE_TIME | OUTCOME | |
|---|---|---|---|---|---|---|
| Min. : 1.0 | Min. :18.00 | MALE :224 | Min. :2020-01-10 15:52:20 | Min. :2020-01-23 09:09:23 | CURED :201 | |
| 1st Qu.: 94.5 | 1st Qu.:46.00 | FEMALE:151 | 1st Qu.:2020-02-01 19:27:40 | 1st Qu.:2020-02-11 13:39:21 | DECEASED:174 | |
| Median :188.0 | Median :62.00 | Median :2020-02-04 22:30:34 | Median :2020-02-16 17:40:07 | |||
| Mean :188.0 | Mean :58.83 | Mean :2020-02-04 20:13:51 | Mean :2020-02-15 16:42:59 | |||
| 3rd Qu.:281.5 | 3rd Qu.:70.00 | 3rd Qu.:2020-02-10 04:11:10 | 3rd Qu.:2020-02-19 11:47:14 | |||
| Max. :375.0 | Max. :95.00 | Max. :2020-02-17 21:30:07 | Max. :2020-03-04 16:21:51 |
Liczba unikatowych identyfikatorów pacjentów wynosi 375.
Pozostałe 74 kolumn zawiera wartości atrybutów, które reprezentują rodzaje miar zebranych podczas każdego badania.
Nazwy kolumn zostały poddane następującym transformacjom:
(%) zamieniono na suffix _percent(#) usunięto i myślnika - zamieniono na podkreślenie dolne _ ze względu na łatwość odwołania się do kolumnPoniższy blok kodu szczegółowo ukazuje zastosowane transformacje, jednocześnie prezentowana są ostateczne nazwy kolumn wyników badań.
df <- df %>%
rename("NT-proBNP" = "Amino-terminal brain natriuretic peptide precursor(NT-proBNP)") %>%
rename("2019-nCov-detection" = "2019-nCoV nucleic acid detection") %>%
rename("aPPT" = "Activation of partial thromboplastin time") %>%
rename("ALP" = "Alkaline phosphatase") %>%
rename("AAT" = "aspartate aminotransferase") %>%
rename("FDPs" = "Fibrin degradation products") %>%
rename("ALT" = "glutamic-pyruvic transaminase") %>%
rename("hs-CRP" = "High sensitivity C-reactive protein") %>%
rename("hs-cTnI" = "Hypersensitive cardiac troponinI") %>%
rename("HCV-abs-quant" = "HCV antibody quantification") %>%
rename("HIV-abs-quant" = "HIV antibody quantification") %>%
rename("INR" = "International standard ratio") %>%
rename("LDH" = "Lactate dehydrogenase") %>%
rename("MCH" = "mean corpuscular hemoglobin") %>%
rename("MCHC" = "mean corpuscular hemoglobin concentration") %>%
rename("MCV" = "mean corpuscular volume") %>%
rename("MPV" = "Mean platelet volume") %>%
rename("P-LCR" = "platelet large cell ratio") %>%
rename("PDW" = "PLT distribution width") %>%
rename("q-t-pallidum-abs" = "Quantification of Treponema pallidum antibodies") %>%
rename("RCDW-SD" = "RBC distribution width SD") %>%
rename("RBC_count" = "Red blood cell count") %>%
rename("RCDW" = "Red blood cell distribution width") %>%
rename("TNF-alfa" = "Tumor necrosis factorα") %>%
rename("WBC_count" = "White blood cell count") %>%
rename("gamma-GT" = "γ-glutamyl transpeptidase") %>%
rename_with(~str_c(str_replace(.x, "\\(%\\)", ""), "_percent"), contains("(%)")) %>%
rename_with(~str_replace(.x, "\\(#\\)", "")) %>%
rename_with(tolower, matches("^[[:upper:]]{1}[[:lower:]]+", ignore.case = FALSE)) %>%
rename_with(~str_replace_all(.x, "[ |-]", "_"))
str_sort(colnames(df[,-(1:7)]))
## [1] "2019_nCov_detection" "AAT" "albumin"
## [4] "ALP" "ALT" "antithrombin"
## [7] "aPPT" "basophil_count" "basophil_percent"
## [10] "calcium" "corrected_calcium" "creatinine"
## [13] "D_D_dimer" "direct_bilirubin" "eGFR"
## [16] "eosinophil_count" "eosinophils_percent" "ESR"
## [19] "FDPs" "ferritin" "fibrinogen"
## [22] "gamma_GT" "globulin" "glucose"
## [25] "HBsAg" "HCO3_" "HCV_abs_quant"
## [28] "hematocrit" "hemoglobin" "HIV_abs_quant"
## [31] "hs_CRP" "hs_cTnI" "indirect_bilirubin"
## [34] "INR" "interleukin_10" "interleukin_1β"
## [37] "interleukin_2_receptor" "interleukin_6" "interleukin_8"
## [40] "LDH" "lymphocyte_count" "lymphocyte_percent"
## [43] "MCH" "MCHC" "MCV"
## [46] "monocytes_count" "monocytes_percent" "MPV"
## [49] "neutrophils_count" "neutrophils_percent" "NT_proBNP"
## [52] "P_LCR" "PDW" "PH_value"
## [55] "platelet_count" "procalcitonin" "prothrombin_activity"
## [58] "prothrombin_time" "q_t_pallidum_abs" "RBC_count"
## [61] "RCDW" "RCDW_SD" "serum_chloride"
## [64] "serum_potassium" "serum_sodium" "thrombin_time"
## [67] "thrombocytocrit" "TNF_alfa" "total_bilirubin"
## [70] "total_cholesterol" "total_protein" "urea"
## [73] "uric_acid" "WBC_count"
Niniejsza sekcja jest poświęcona wstępnemu przetwarzaniu zbiorów danych, ze szczególnym uwzględnieniem usuwania i scalania wybranych rekordów.
W powyższej głównej zauważono wystąpienia wartości brakujących dla kolumny RE_DATE. Szybkie sprawdzenie wykazuje, że dla wierszy, gdzie nie ma atrybut RE_DATE przyjmuje wartość ‘NA’, wartości wszystkich atrybutów pochodzących z badań krwi, również przyjmują wartość ‘NA’.
all(sapply(df[is.na(df$RE_DATE), -c(1,3:7)], is.na))
## [1] TRUE
Fakt, że wiersze te są właściwie puste, jest podstawą do ich usunięcia ze zbioru.
df <- df[!is.na(df$RE_DATE),]
Po tej zmianie w zbiorze pozostało 6106 wierszy i 361 unikalnych identyfikatorów pacjentów.
Wstępna analiza danych sugeruje wskazuje, że wiele wartości atrybutów jest oznaczona wartością ‘NA’. Poniższy wykresy wskazuje, ilość wystąpień rekordów badań z daną liczbą wartości (różnych od NA).
Z powyższego histogramu można wyciągnąć kilka wniosków
W poprzedniej sekcji stwierdzono, że można przeanalizować współwystępowanie ze sobą poszczególnych atrybutów w rekordach. W związku z tym tabela wyników badań zostanie przekształcona do zbioru binarnych transakcji.
transactions <- sapply(df[,-(1:7)], function(x) !is.na(x))
Sprawdzona zostanie ilość unikalnych transakcji.
length(unique(apply(transactions, 1, function(x) which(x))))
## [1] 176
W zbiorze 6106 transakcji jest zaledwie 176 różnych typów.
Aby sprawdzić, czy atrybuty występują w grupach, wyszukiwane są maksymalne domknięte zbiory częste, dla minimalnej wartości support równej 5%.
itemsets <- apriori(transactions, parameter = list(target="closed frequent itemsets", support=.05, minlen=1, maxlen=30, maxtime=60))
## Apriori
##
## Parameter specification:
## confidence minval smax arem aval originalSupport maxtime support minlen
## NA 0.1 1 none FALSE TRUE 60 0.05 1
## maxlen target ext
## 30 closed frequent itemsets TRUE
##
## Algorithmic control:
## filter tree heap memopt load sort verbose
## 0.1 TRUE TRUE FALSE TRUE 2 TRUE
##
## Absolute minimum support count: 305
##
## set item appearances ...[0 item(s)] done [0.00s].
## set transactions ...[74 item(s), 6106 transaction(s)] done [0.01s].
## sorting and recoding items ... [63 item(s)] done [0.00s].
## creating transaction tree ... done [0.00s].
## checking subsets of size 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 done [33.21s].
## filtering closed item sets ... done [21.72s].
## sorting transactions ... done [0.00s].
## writing ... [130 set(s)] done [0.33s].
## creating S4 object ... done [1.29s].
mc_itemsets <- itemsets[is.maximal(itemsets)]
inspect(sort(mc_itemsets, by = 'support'))
## items support transIdenticalToItemsets count
## [1] {hemoglobin,
## eosinophils_percent,
## basophil_percent,
## platelet_count,
## monocytes_percent,
## RCDW,
## neutrophils_percent,
## MCV,
## hematocrit,
## WBC_count,
## MCHC,
## lymphocyte_count,
## RBC_count,
## eosinophil_count,
## neutrophils_count,
## MPV,
## RCDW_SD,
## lymphocyte_percent,
## P_LCR,
## monocytes_count,
## PDW,
## basophil_count,
## MCH,
## thrombocytocrit} 0.13609564 0.1354406 831
## [2] {glucose} 0.12692434 0.0000000 775
## [3] {serum_chloride,
## ALP,
## albumin,
## total_bilirubin,
## indirect_bilirubin,
## total_protein,
## urea,
## corrected_calcium,
## serum_potassium,
## direct_bilirubin,
## total_cholesterol,
## AAT,
## uric_acid,
## HCO3_,
## calcium,
## LDH,
## globulin,
## gamma_GT,
## hs_CRP,
## serum_sodium,
## ALT,
## eGFR,
## creatinine} 0.11021946 0.1094006 673
## [4] {hs_cTnI} 0.08303308 0.0000000 507
## [5] {2019_nCov_detection} 0.08205044 0.0000000 501
## [6] {NT_proBNP} 0.07779234 0.0000000 475
## [7] {procalcitonin} 0.07517196 0.0000000 459
## [8] {PH_value} 0.06288896 0.0000000 384
## [9] {ESR} 0.06272519 0.0000000 383
## [10] {prothrombin_time,
## antithrombin,
## prothrombin_activity,
## fibrinogen,
## thrombin_time,
## D_D_dimer,
## FDPs,
## INR,
## aPPT} 0.05371765 0.0000000 328
Otrzymaliśmy 10 maksymalnych domkniętych zbiorów częstych, z czego 7 to zbiory jednoelementowe. Pozostałe 3 zbiory (1, 3 i 10) zawierają wiele elementów. Na podstawie elementów składowych, zbiory wieloelemntowe zostały zakwalifikowane następująco:
Na podstawie uzyskanego wyniku można wnioskować, że ilość elementów w rekordzie może wynikać z procesu technologicznego analizowania próbek krwi. Sugeruje to, że możliwe będzie scalanie rekordów, np na podstawie dnia wykonania badania (jeśli jednemu pacjentowi przeprowadzono wiele badań w ciągu dnia).
W tej sekcji przeprowadzona zostanie analiza występowania badań w czasie. Ponadto rekordy zostaną pogrupowane według przynależności do zbiorów wyznaczonych w poprzedniej sekcji oraz dwóch dodatkowych klas NONE jeśli rekord nie należy do żadnej kategorii i MULTIPLE jeśli rekord należy do więcej niż jednej kategorii.
Poniższe blok kodu generuje oetykietowany zbiór rekordów badań, ograniczony jedynie do podstawowych informacji.
class_labels <- as(items(sort(mc_itemsets, by="support")), "list")
class_labels <- sapply(class_labels, function(x) x[1])
class_labels[1] <- "Complete blood count"
class_labels[3] <- "Blood biochemistry"
class_labels[10] <- "Coagulation"
classes <- as(items(sort(mc_itemsets, by="support")), "matrix")
labeled <- df %>% rowwise() %>%
mutate(CLASS = paste(
which(
apply(
(matrix(
!is.na(c_across(-(1:7))),
nrow=nrow(classes),
ncol=ncol(classes),
byrow=TRUE,
dimnames=dimnames(classes)
) & classes) == classes,
1,
all)
), collapse=""),
.after=OUTCOME ) %>%
mutate(CLASS = if(CLASS != "" && as.integer(CLASS)>10) "MULTIPLE" else CLASS) %>%
mutate(CLASS = if(CLASS != "" && CLASS != "MULTIPLE") class_labels[as.integer(CLASS)] else CLASS) %>%
mutate(CLASS = str_replace(CLASS, "^$", "NONE")) %>%
mutate(CLASS = str_trim(CLASS)) %>%
mutate(CLASS = factor(CLASS)) %>%
rowwise() %>%
mutate(SIZE = sum(!is.na(c_across(-c(1:8)))), .after=CLASS) %>%
select(1:9) %>%
ungroup()
knitr::kable(
labeled %>% group_by(CLASS) %>% summarize(count = n(), .groups="drop") %>% arrange(desc(count))
)
| CLASS | count |
|---|---|
| NONE | 1433 |
| Complete blood count | 816 |
| glucose | 584 |
| Blood biochemistry | 507 |
| 2019_nCov_detection | 500 |
| MULTIPLE | 485 |
| hs_cTnI | 386 |
| PH_value | 375 |
| ESR | 368 |
| Coagulation | 300 |
| NT_proBNP | 190 |
| procalcitonin | 162 |
Powyższa tabela prezentuje ilość rekordów przydzielonych do każdej z klas.
Poniższy wykres prezentuje badania danej kategorii według daty wykonania. Dodatkowo każdy punkt jest opisany identyfikatorem pacjenta.
Z powyższego wykresu można wywnioskować kilka rzeczy:
RE_DATE. Biorąc pod uwagę fakt, że takie punkty najczęściej przynależą do różnych pacjentów i do tej samej kategorii, należy wnioskować, że poszczególne rodzaje badań były wykonywane równolegle dla wielu pacjentów.Jednocześnie nie znaleziono żadnych czynników, które podważają sens scalania rekordów.
Kolejna analiza dotyczy ilości różnych atrybutów zbadanych u każdego pacjenta. W tym celu wyznaczamy wektor wartości ostatnich badań każdego z pacjentów, tj wynik każdego badania jest najmłodszy i różny od NA, jeśli dany pacjent nie ma przypisanego żadnego rekordu zawierającego wartość danego atrybutu, to wartość ta nadal będzie oznakowana jako NA.
Powyższy histogram obrazuje rozkład ilości badań wykonanych pacjentom. Zauważalna jest grupa wartości odstających - pacjentów którym wykonano znacząco mniej rodzajów badań. Rekordy tych pacjentów zostaną usunięte ze zbioru danych.
W celu usunięcia pacjentów z mniejszą ilością badań, zostanie wyznaczony próg minimalnej ilości badań. Próg ten przyjmuje wartość między 0 a 1, należy go interpretować jako minimalną procentową ilość badań, które musi mieć wykonany pacjent. Maksimum stanowi ilość wszystkich badanych atrybutów, czyli 74.
Wartość progu została wyznaczona analitycznie, w następujący sposób; dla każdej wartości progu z zakresu od 0.5 do 1 z krokiem 0.025, wyznaczona został procent pozostałych (nie odrzuconych) pacjentów, w stosunku do liczby początkowej, a także procent kolumn, które nie zawierają żadnej wartości NA.
Uzyskane dane zostały przedstawione na poniższym wykresie.
Wybrana wartość została oznaczona na wykresie czarną przerywaną linią, wynosi 0.65 i odpowiada 48 atrybutom. Taka wartość progowa pozwala na zachowanie znaczącej większości pacjentów w zbiorze, uzyskanie lepszego efektu poprzez jej zwiększenie, wiązałoby się ze znaczącym zmniejszeniem ilości pacjentów w zbiorze wynikowym.
patients_to_be_removed <- patient_last_results %>%
filter(number_of_tests < (ncol(df)-7) * threshold) %>%
select(PATIENT_ID)
df <- df %>% filter(PATIENT_ID %!in% patients_to_be_removed$PATIENT_ID)
Po usunięciu rekordów pacjentów, u których przeprowadzono mało badań, pozostało 354 unikalnych pacjentów.
Poniższy histogram prezentuje ilość kolumn, według ilości brakujących wartości badań w każdej kolumnie, biorąc pod uwagę wszystkie wyniki każdego z pacjentów. Dodatkowo nazwy kolumn zostały oznaczone według przynależności do jednej z grup badań (sekcja “Współwystępowanie badań”), lub opisane wartością NONE, jeśli nie należą do żadnej z nich.
Na powyższym wykresie możemy zaobserwować, że znacząca część badań została wykonana wszystkim pozostałym w zbiorze pacjentom. Dodatkowo, wszystkie testy z grupy biochemii krwi i morfologii zostały przeprowadzone u większości pacjentów.
W kolejnych krokach można rozważyć odrzucenie pacjentów, którzy nie mają wykonanych wszystkich testów w tych dwóch grupach.
W związku z brakiem wykrytych przeciwwskazań, w tym kroku przeprowadzono scalanie wyników badań każdego pacjenta, według dnia jego wykonania, tj. wszystkie rekordy z danego dnia zostaną scalone w jeden rekord, który zawiera wszystkie, najbardziej aktualne wyniki z danego dnia.
df <- df %>%
mutate(RE_DATE = floor_date(RE_DATE, '1 day')) %>%
group_by(PATIENT_ID, RE_DATE) %>%
fill(everything()) %>%
summarise_all(last) %>%
ungroup()
Po tej transformacji, w zbiorze danych pozostało 1688 rekordów.
Powyższy histogram prezentuje ilość rekordów zawierających daną ilość wyników badań. Pomimo znacznej ilości rekordów zawierających tylko jeden wynik badania, obserwujemy wzrost w ilości rekordów zawierających ponad 40 wyników badań, w porównaniu do wyników przed agregacją.
Ta sekcja jest poświęcona analizie danych w zbiorze.
Poniższy wykres prezentuje ilość przypadków z dany rezultatem leczenia w stosunku do płci pacjenta. Obserwujemy relatywnie małą ilość zmarłych kobiet w stosunku do zmarłych mężczyzn.
Poniższy wykres przedstawia rozkład wieku w zależności od wyniku leczenia i płci pacjenta. Większość ze zmarłych pacjentów ma powyżej 60 lat, co sugeruje że grupa starszych pacjentów jest bardziej narażona na śmierć w wyniku choroby spowodowanej zakażeniem wirusem.
Poniższy zbiór wykresów przedstawia rozkład poszczególnych wyników badań, w zależności od rezultatu leczenia. Część wykresów jest nieczytelna przez występowanie wartości odstających (outlierów).
W niektórych wypadkach możemy zaobserwować istotne różnice między zbiorami pacjentów wyleczonych i zmarłych, są to między innymi parametry takie jak: albumin, hs_CRP, lymphoocytes_percent i monocythes_percent.
Poniższy wykres przedstawia wartość współczynnika korelacji Pearsona między wszystkimi parametrami liczbowymi w zbiorze. Parametry nominalne GENDER i OUTCOME, ze względu na to zę mają tylko dwie wartości, zostały zakodowane jako isMALE i isCURED. Dodatkowo, ze parametr 2019_nCov_detection ze względu na stałą wartość, został zamieniony na parametr has_nCOV_test, który indykuje czy w danym badaniu wykonano taki test.
Ponadto w poniższej tabeli zebrano 30 par atrybutów z największym współczynnikiem korelacji Pearsona.
| rowname | colname | value |
|---|---|---|
| MPV | P_LCR | 0.99 |
| INR | prothrombin_time | 0.99 |
| platelet_count | thrombocytocrit | 0.98 |
| HBsAg | interleukin_6 | 0.98 |
| direct_bilirubin | total_bilirubin | 0.98 |
| lymphocyte_percent | neutrophils_percent | -0.98 |
| procalcitonin | q_t_pallidum_abs | 0.95 |
| D_D_dimer | FDPs | 0.95 |
| P_LCR | PDW | 0.95 |
| MPV | PDW | 0.94 |
| ALT | interleukin_1β | 0.93 |
| lymphocyte_count | monocytes_count | 0.88 |
| serum_chloride | serum_sodium | 0.86 |
| eosinophil_count | eosinophils_percent | 0.86 |
| AAT | interleukin_1β | 0.86 |
| hematocrit | hemoglobin | 0.84 |
| MCH | MCV | 0.82 |
| indirect_bilirubin | total_bilirubin | 0.80 |
| interleukin_6 | interleukin_8 | 0.80 |
| RCDW | RCDW_SD | 0.79 |
| aPPT | prothrombin_time | 0.79 |
| monocytes_percent | neutrophils_percent | -0.77 |
| eGFR | urea | -0.77 |
| creatinine | urea | 0.75 |
| albumin | calcium | 0.74 |
| isCURED | neutrophils_percent | -0.73 |
| isCURED | lymphocyte_percent | 0.72 |
| neutrophils_count | neutrophils_percent | 0.71 |
| albumin | total_protein | 0.70 |
| lymphocyte_percent | neutrophils_count | -0.70 |
Można zaobserwować wiele mocno skorelowanych parametrów, m.in:
MPV, P-LCR i PDW odnoszą się do wielkości płytek krwi (odpowiednio: mean platelet volume, platelet - large cell ratio i platelet distribution width), więc ich wysokie skorelowanie jest łatwe do wyjaśnienia.lymphocytes_percent i neutrophils_percent - określają proporcje białych krwinek danego typu w krwi pacjentaINR z definicji jest właściwie liniowo zależna od prothrombin_timedirect_bilirubin jest składową total_bilirubinDodatkowo sprawdzona zostanie korelacja poszczególnych parametrów z wynikiem leczenia chorego (isCured). Poniższa tabela zawiera wartości korelacji Pearsona wraz z przedziałem ufności i p-wartością (wszystkie z pokazanych korelacji są istotne statystycznie) dla 15 najmocniej skorelowanych parametrów.
| attribute | estimate | p.value | conf.low | conf.high |
|---|---|---|---|---|
| neutrophils_percent | -0.7301461 | 0 | -0.7586392 | -0.6988654 |
| lymphocyte_percent | 0.7234883 | 0 | 0.6915912 | 0.7525691 |
| albumin | 0.6880859 | 0 | 0.6524294 | 0.7207028 |
| prothrombin_activity | 0.6547754 | 0 | 0.6084767 | 0.6966318 |
| hs_CRP | -0.6509896 | 0 | -0.6910468 | -0.6069460 |
| D_D_dimer | -0.6376192 | 0 | -0.6819778 | -0.5885867 |
| LDH | -0.6270327 | 0 | -0.6648056 | -0.5860615 |
| neutrophils_count | -0.6083675 | 0 | -0.6470960 | -0.5665074 |
| FDPs | -0.6042409 | 0 | -0.6689583 | -0.5304310 |
| AGE | -0.5761382 | 0 | -0.6071595 | -0.5433633 |
| calcium | 0.5703332 | 0 | 0.5257539 | 0.6117881 |
| platelet_count | 0.5419441 | 0 | 0.4952125 | 0.5855487 |
| monocytes_percent | 0.5415337 | 0 | 0.4947996 | 0.5851444 |
| eGFR | 0.4909936 | 0 | 0.4402769 | 0.5385870 |
| urea | -0.4841428 | 0 | -0.5321758 | -0.4330031 |
Z powyższej tabeli wynika, że wpływ na wynik choroby pacjenta mają:
lymphocyte i niska proporcja neutrophils)prothrombin_activity, D_D_dimer, FDPs)hs_CRP i LDH których podwyższony stan wskazuje na dużą ilość uszkodzonych komórekalbumin - białek odpowiadających za transport odczynników we krwi (w tym leków) i regulację ilości wody we krwiPoniższy wykres pokazuje ilość pacjentów przyjętych i wypisanych danego dnia ze szpitala, z podziałem na wynik leczenia.
Analiza przyjęć i wypisów nie przynosi żadnych istotnych wniosków. Obserwujemy jedynie zwiększoną ilość wypisów wyleczonych pacjentów między 16 a 20 lutego. Obserwowane luki w czasach przyjęć i wypisów mogą wynikać z podziału zbioru danych na część treningową (analizowaną) i testową (wyłączoną z niniejszej analizy).
Poniższy wykresy prezentują odpowiednio rozkład czasu pobytu pacjentów w szpitalu w zależności od wyników leczenia a także histogram długości pobytu w szpitalu z podziałem na wynik leczenia.
Obserwujemy, że w grupy pacjentów, którzy zmarli, czas pobytu w szpitalu wynosił do 10 dni (75% przypadków). Tę cechę można potencjalnie wykorzystać przy przygotowaniu klasyfikatora.
Jednocześnie poczyniona obserwacja każe się zastanowić nad dokładnością klasyfikatora zaproponowanego przez autorów artykułu, z którego pochodzą dane. Badacze twierdzą, że dokładność wynosi ponad 90% nawet 10 dni przed poznaniem dokładnego wyniku leczenia. Z drugiej strony, odrzucenie badań które powstały później, niż 10 dni przed poznaniem ostatecznego wyniku, skutkuje odrzuceniem danych o większości zmarłych pacjentów, co pozostawia niezbalansowany zbiór danych. Jak wiadomo dokładność klasyfikatora nie jest odpowiednią do zastosowania metryką przy klasyfikacji niezbalansowanych zbiorów, a wartość miary Kappa nie została podana w treści artykułu, co utrudnia ocenienie jakości zaprezentowanego w artykule modelu.
Niniejsza sekcja opisuje proces tworzenia klasyfikatora, którego zadaniem jest ocena, czy dany pacjent przeżył chorobę spowodowaną zakażeniem COVID-19.
Dla tego celu, zbiór wyników badań zostanie zredukowany do danych pacjentów, zawierając ostatni, tj. najbardziej aktualny wynik badania. Wcześniej, ze zbioru zostaną usunięte te wyniki badań, które zostały uzyskane przed wypisem pacjenta ze szpitala. Dodatkowo usunięte zostanę atrybuty PATIENT_ID, ADMISSION_TIME, DISCHARGE_TIME i RE_DATE oraz 2019_nCOV-detection (ze względu na stałe wartości).
patients <- df %>%
filter(RE_DATE < DISCHARGE_TIME) %>%
group_by(PATIENT_ID) %>%
fill(everything()) %>%
summarise_all(last) %>%
ungroup() %>%
select(-PATIENT_ID, -ADMISSION_TIME, -DISCHARGE_TIME, -RE_DATE) %>%
select(-'2019_nCov_detection')
W związku z tym, że dla części pacjentów nie ma dostępnych wszystkich wyników badań, niezbędne jest dalsze przetwarzanie zbioru danych.
Grupa cech “Biochemia krwi” (sekcja Współwystępowanie kategorii) została określona jako minimalny zbiór cech, które powinny być obecne w każdym rekordzie. Ten wybór jest uzasadniony tym, że autorzy artykułu źródłowego wśród najbardziej znaczących cech wskazali LDH i hs_CRP, które należą do tej grupy.
Po odrzuceniu rekordów, które nie zawierały wszystkich wskazanych cech, usunięto kolumny, które zawierały wartości NA. W wyniku tego przetwarzania uzyskano czysty zbiór, który może być wykorzystany do trenowania klasyfikatora.
features <- class_labels[[3]]
patients <- patients %>% filter(complete.cases(patients %>% select(all_of(features))))
to.drop <- patients %>%
pivot_longer(4:76) %>%
mutate(value=is.na(value)) %>%
group_by(name) %>%
summarize(NAs = sum(value), .groups="drop") %>%
filter(NAs != 0)
#to.drop %>% arrange(NAs)
patients <- patients %>% select(-to.drop$name)
all(complete.cases(patients))
## [1] TRUE
Ostateczna wersja zbioru zawiera 345 rekordów pacjentów. Poniższa lista zawier natomiast nazwy wszystkich 43 cech wykorzystanych do klasyfikacji.
## [1] "AGE" "GENDER" "hemoglobin"
## [4] "serum_chloride" "eosinophils_percent" "ALP"
## [7] "albumin" "basophil_percent" "total_bilirubin"
## [10] "platelet_count" "monocytes_percent" "indirect_bilirubin"
## [13] "neutrophils_percent" "total_protein" "MCV"
## [16] "hematocrit" "WBC_count" "MCHC"
## [19] "urea" "lymphocyte_count" "RBC_count"
## [22] "eosinophil_count" "corrected_calcium" "serum_potassium"
## [25] "neutrophils_count" "direct_bilirubin" "lymphocyte_percent"
## [28] "total_cholesterol" "AAT" "uric_acid"
## [31] "HCO3_" "calcium" "LDH"
## [34] "monocytes_count" "globulin" "gamma_GT"
## [37] "basophil_count" "MCH" "hs_CRP"
## [40] "serum_sodium" "ALT" "eGFR"
## [43] "creatinine"
Zbalansowanie zbioru prezentuje się następująco
## CURED DECEASED
## 191 154
W kolejnym kroku zbiór został podzielony na testowy i treningowy, odkładając 25% danych do testowania klasyfikatorów.
set.seed(23)
inTraining <- createDataPartition(
y = patients$OUTCOME,
p = .75,
list = FALSE)
patientsTrain <- patients[ inTraining[,1],]
patientsTest <- patients[-inTraining[,1],]
W tej części zostaną wytrenowane i przetestowane dwa klasyfikatory - Random Forest i SVM. W trenowaniu zostanie wykorzystana powtarzalna walidacja krzyżowa, z podziałem na 10 części i trzema powtórzeniami.
W modelu Random Forest optymalizowany będzie parametr mtree określający ilość parametrów, które trafią do drzewa w każdej rundzie podziałów. Jednocześnie ustalono, że wartość parametru określającą ilość drzew w lesie ntree będzie wynosiła 30.
set.seed(23)
tc <- trainControl(method="repeatedcv", number=10, repeats=3)
rf.grid <- expand.grid(mtry = 5:30)
rf.fit <- train(OUTCOME ~ .,
data = patientsTrain,
method = "rf",
preProc = c("center", "scale"),
trControl = tc,
tuneGrid = rf.grid,
ntree = 30)
rf.fit
## Random Forest
##
## 260 samples
## 43 predictor
## 2 classes: 'CURED', 'DECEASED'
##
## Pre-processing: centered (43), scaled (43)
## Resampling: Cross-Validated (10 fold, repeated 3 times)
## Summary of sample sizes: 234, 234, 235, 233, 234, 234, ...
## Resampling results across tuning parameters:
##
## mtry Accuracy Kappa
## 5 0.9783400 0.9561484
## 6 0.9796657 0.9588794
## 7 0.9797170 0.9591510
## 8 0.9809003 0.9613794
## 9 0.9860323 0.9718434
## 10 0.9834682 0.9666227
## 11 0.9847502 0.9692488
## 12 0.9834207 0.9665154
## 13 0.9859848 0.9718002
## 14 0.9795745 0.9585735
## 15 0.9835195 0.9666743
## 16 0.9834207 0.9665467
## 17 0.9859848 0.9717693
## 18 0.9847028 0.9691417
## 19 0.9808566 0.9613880
## 20 0.9834207 0.9666104
## 21 0.9848015 0.9693949
## 22 0.9821861 0.9638227
## 23 0.9808566 0.9611002
## 24 0.9795745 0.9586660
## 25 0.9796220 0.9586125
## 26 0.9783400 0.9560791
## 27 0.9808566 0.9612299
## 28 0.9782925 0.9560051
## 29 0.9770579 0.9534538
## 30 0.9783400 0.9560791
##
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was mtry = 9.
Wyniki uzyskane w trakcie procesu trenowania charakteryzują się wysoką dokładnością i wartością miary Kappa, co sugeruje że klasyfikator może dobrze wykonywać swoje zadanie. W kolejnych krokach te wyniki zostaną zweryfikowane na zbiorze testowym.
set.seed(23)
rf.predict <- predict(rf.fit, newdata=patientsTest)
confusionMatrix(data = rf.predict, patientsTest$OUTCOME)
## Confusion Matrix and Statistics
##
## Reference
## Prediction CURED DECEASED
## CURED 45 2
## DECEASED 2 36
##
## Accuracy : 0.9529
## 95% CI : (0.8839, 0.987)
## No Information Rate : 0.5529
## P-Value [Acc > NIR] : <2e-16
##
## Kappa : 0.9048
##
## Mcnemar's Test P-Value : 1
##
## Sensitivity : 0.9574
## Specificity : 0.9474
## Pos Pred Value : 0.9574
## Neg Pred Value : 0.9474
## Prevalence : 0.5529
## Detection Rate : 0.5294
## Detection Prevalence : 0.5529
## Balanced Accuracy : 0.9524
##
## 'Positive' Class : CURED
##
Klasyfikator pomylił się w 4 przypadkach (na 85) osiągając dokładność na poziomie 95%.
W związku z tym, że model Random Forest jest łatwo interpretowalny, w łatwy sposób można uzyskać ważność cech, która jest przedstawiona w tabeli poniżej.
| Overall | |
|---|---|
| LDH | 25.19929 |
| neutrophils_percent | 24.16844 |
| lymphocyte_percent | 16.89785 |
| hs_CRP | 16.28050 |
| lymphocyte_count | 10.97626 |
Należy zauważyć, że cechy LDH i hs_CRP, a także lymphocyte_percent są ważnymi czynnikami w wytrenowanym klasyfikatorze, co jest w zgodzie z wnioskami autorów ze źródłowego artykułu.
Dla modelu Support Vector Machine z kernelem liniowym, optymalizowany jest parametr kosztu.
set.seed(23)
# svmLinear2
svm.grid <- expand.grid(cost = c(.25, .5, .75, 1, 1.25))
svm.fit <- train(OUTCOME ~ .,
data = patientsTrain,
method = "svmLinear2",
preProc = c("center", "scale"),
trControl = tc,
tuneGrid = svm.grid
)
svm.fit
## Support Vector Machines with Linear Kernel
##
## 260 samples
## 43 predictor
## 2 classes: 'CURED', 'DECEASED'
##
## Pre-processing: centered (43), scaled (43)
## Resampling: Cross-Validated (10 fold, repeated 3 times)
## Summary of sample sizes: 234, 234, 235, 233, 234, 234, ...
## Resampling results across tuning parameters:
##
## cost Accuracy Kappa
## 0.25 0.9706401 0.9402500
## 0.50 0.9720722 0.9433396
## 0.75 0.9696068 0.9383617
## 1.00 0.9592953 0.9172186
## 1.25 0.9528851 0.9042441
##
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was cost = 0.5.
Podobnie jak model Random Forest, model SVM również osiąga wysokie wyniki w klasyfikacji na etapie treningu.
set.seed(23)
svm.predict <- predict(svm.fit, newdata=patientsTest)
confusionMatrix(data = svm.predict, patientsTest$OUTCOME)
## Confusion Matrix and Statistics
##
## Reference
## Prediction CURED DECEASED
## CURED 45 1
## DECEASED 2 37
##
## Accuracy : 0.9647
## 95% CI : (0.9003, 0.9927)
## No Information Rate : 0.5529
## P-Value [Acc > NIR] : <2e-16
##
## Kappa : 0.9288
##
## Mcnemar's Test P-Value : 1
##
## Sensitivity : 0.9574
## Specificity : 0.9737
## Pos Pred Value : 0.9783
## Neg Pred Value : 0.9487
## Prevalence : 0.5529
## Detection Rate : 0.5294
## Detection Prevalence : 0.5412
## Balanced Accuracy : 0.9656
##
## 'Positive' Class : CURED
##
Na etapie testu model SVM popełnił jedynie 3 błędy (ponad 96% dokładności), przez co można go uznać za minimalnie lepszy od modelu Random Forest.
W tej sekcji wyniki badań pacjentów zostaną poddane analizie metodą regresji Coxa.
Zbiór danych został poddany następującym zmianom:
RE_DATE późniejszy niż DISCHARGE_TIMETIME która określa liczbę dni, które upłynęły od dokonania badania, do uzyskania rezultatu leczenia (na potrzeby modelu Coxa)OUTCOME i GENDER jako wartości numerycznePATIENT_ID, ADMISSION_TIME, DISCHARGE_TIME, RE_DATE oraz kolumnę 2019_nCov_detection ze względu na identyczne wartościW kolejnych krokach dla każdego z atrybutów (niezależnie) wykonano analizę tzw. Hazard Ratio (HR) przy pomocy modelu proporcjonalnego hazardu Coxa. Hazard Ratio jest wyjaśnione następująco (źródło):
Put another way, a hazard ratio above 1 indicates a covariate that is positively associated with the event probability, and thus negatively associated with the length of survival.
Poniższa tabela prezentuje czynniki zwiększające ryzyko śmierci pacjenta. Odfiltrowane zostały czynniki nieistotne statystycznie (p-wartość > 0.05) oraz te, gdzie parametr HR nie przekroczył wartości 1.1.
| beta | HR | wald.test | p.value | |
|---|---|---|---|---|
| basophil_count | 15.9100 | 8.128e+06 | 47.32 | 0.0000000 |
| HCV_abs_quant | 0.6568 | 1.929e+00 | 6.34 | 0.0118137 |
| MPV | 0.5709 | 1.770e+00 | 162.75 | 0.0000000 |
| serum_potassium | 0.2852 | 1.330e+00 | 63.53 | 0.0000000 |
| PH_value | 0.2822 | 1.326e+00 | 5.36 | 0.0205911 |
| INR | 0.2714 | 1.312e+00 | 96.76 | 0.0000000 |
| RCDW | 0.2019 | 1.224e+00 | 130.65 | 0.0000000 |
| PDW | 0.1923 | 1.212e+00 | 145.21 | 0.0000000 |
| neutrophils_count | 0.1067 | 1.113e+00 | 369.89 | 0.0000000 |
| neutrophils_percent | 0.1022 | 1.108e+00 | 259.56 | 0.0000000 |
Parametrem, który w największym stopniu zwiększa ryzyko śmierci, według przygotowanego modelu jest basophil_count. Jednocześnie uzyskany wynik zdaje się być wątpliwy, gdyż basophil_count ma wartość parametru HR o sześć rzędów wielkości większą, niż kolejny parametr w rankingu.
Pozostałe złe prognostyki, dotyczą między innymi zwiększonej obecności neutrofilów, krzepliwości krwi (wysokie INR) a także jakość komórek krwi (MPV, RCDW, PDW).
Poniższa tabela przedstawia dobre prognostyki jeśli chodzi o skutki leczenia. Ponownie odfiltrowano czynniki nieistotne statystycznie (p-wartość > 0.05), oraz pozostawiono wartości, gdzie parametr HR ma wartość poniżej 0.9.
| beta | HR | wald.test | p.value | |
|---|---|---|---|---|
| HCO3_ | -0.1079 | 0.8977000 | 107.45 | 0e+00 |
| albumin | -0.1363 | 0.8726000 | 288.36 | 0e+00 |
| lymphocyte_percent | -0.1455 | 0.8646000 | 250.00 | 0e+00 |
| monocytes_percent | -0.2517 | 0.7775000 | 199.03 | 0e+00 |
| total_cholesterol | -0.5328 | 0.5870000 | 83.96 | 0e+00 |
| GENDER | -0.8068 | 0.4463000 | 81.22 | 0e+00 |
| eosinophils_percent | -1.1360 | 0.3211000 | 71.02 | 0e+00 |
| basophil_percent | -1.5800 | 0.2059000 | 28.46 | 1e-07 |
| lymphocyte_count | -2.1470 | 0.1169000 | 224.40 | 0e+00 |
| calcium | -3.6360 | 0.0263700 | 250.16 | 0e+00 |
| eosinophil_count | -6.6720 | 0.0012660 | 26.65 | 2e-07 |
| thrombocytocrit | -8.6920 | 0.0001678 | 142.03 | 0e+00 |
Do pozytywnych prognostyków można zaliczyć:
Wśród czynników obniżających współczynnik ryzyka, znajduje się też basophil_percent co wydaje się być szczególnie intrygujące w kontekście wykrytych negatywnych prognostyków.
Wśród wykrytych pozytywnych prognostyków, 3 ostatnie wydają się mieć bardzo istotny wpływ na obniżenie ryzyka śmierci pacjenta, jednocześnie należy zwrócić uwagę na nadzwyczajnie niskie wartości parametru HR i podchodzić do nich z dystansem.
Niniejsza sekcja zawiera dodatkowe informacje o technicznych parametrach raportu
unique(c(.packages(), loadedNamespaces()))
## [1] "survival" "caret" "lattice" "gridExtra" "plotly"
## [6] "tibble" "broom" "purrr" "ggplot2" "stringr"
## [11] "lubridate" "dplyr" "tidyr" "arules" "Matrix"
## [16] "httr" "readxl" "stats" "graphics" "grDevices"
## [21] "utils" "datasets" "methods" "base" "Rcpp"
## [26] "class" "digest" "ipred" "foreach" "R6"
## [31] "cellranger" "plyr" "backports" "stats4" "e1071"
## [36] "evaluate" "highr" "pillar" "rlang" "curl"
## [41] "lazyeval" "data.table" "rpart" "rmarkdown" "labeling"
## [46] "splines" "gower" "htmlwidgets" "munsell" "compiler"
## [51] "xfun" "pkgconfig" "htmltools" "nnet" "tidyselect"
## [56] "prodlim" "codetools" "randomForest" "viridisLite" "crayon"
## [61] "withr" "MASS" "recipes" "ModelMetrics" "grid"
## [66] "nlme" "jsonlite" "gtable" "lifecycle" "magrittr"
## [71] "pROC" "scales" "stringi" "farver" "reshape2"
## [76] "timeDate" "ellipsis" "generics" "vctrs" "lava"
## [81] "iterators" "tools" "glue" "crosstalk" "yaml"
## [86] "colorspace" "knitr"